package utilities.control.mouse;

import java.awt.Container;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

import utilities.control.enums.MouseEventType;

/** <code>Mouse</code><br>
 * <br>
 * The <code>Mouse</code> class is an abstract class that implements the <code>MouseListener</code> 
 * and <code>MouseWheelListener</code> interfaces. It keeps track of <code>MouseEvents</code> and
 * <code>MouseWheelEvents</code> that occur and stores them in descending order of occurrence.<br>
 * <br>
 *  
 * @author CoconutTank
 *
 */
public abstract class Mouse implements MouseListener, MouseWheelListener, MouseMotionListener
{
	/*******************************************************************
	 * 
	 * CLASS VARIABLES
	 * 
	 *******************************************************************/
	
	/** <code><b>maxEvents</b></code><br>
	 * <br>
	 * The upper limit of how many <code>MouseEvents</code> the <code>Mouse</code> object will keep track of.<br>
	 * An upper limit is specified for <code>MouseEvents</code> of each type.<br>
	 * <br>
	 * 
	 */
	private int[] maxEvents = null;
	
	/** <code><b>allMouseEvents</b></code><br>
	 * <br>
	 * Stores all recorded <code>MouseEvents</code>.<br>
	 * Each element of the array is a <code>LinkedList</code> for each <code>MouseEvent</code> type.<br>
	 * <code>MouseWheelEvents</code> are not stored in <code>allMouseEvents</code>; see 
	 * <code>allMouseWheelEvents</code>.<br>
	 * <br>
	 * 
	 */
	private LinkedList<MouseEvent>[] allMouseEvents = null;
	
	/** <code><b>allMouseWheelEvents</b></code><br>
	 * <br>
	 * Stores all recorded <code>MouseWheelEvents</code>.<br>
	 * 
	 */
	private LinkedList<MouseWheelEvent> allMouseWheelEvents = null;
	
	/** <code><b>parent</b></code><br>
	 * <br>
	 * The <code>Container</code> object which has registered this <code>Mouse</code>. It is
	 * highly recommended that the methods provided by <code>Mouse</code> are used for 
	 * <code>Container</code> interaction.<br>
	 * <br>
	 * 
	 */
	private Container parent;
	
	/*******************************************************************
	 * 
	 * CONSTRUCTORS
	 * 
	 *******************************************************************/
	
	/**
	 * 
	 * @param maxMouseDraggedEventsToRecord TODO
	 * @param maxMouseMovedEventsToRecord TODO
	 * @param mouseSource TODO
	 * @param maxEventsToRecord
	 * @param recordMouseClicked
	 * @param recordMousePressed
	 * @param recordMouseReleased
	 * @param recordMouseEntered
	 * @param recordMouseExited
	 * @param recordMouseWheelMoved
	 */
	@SuppressWarnings("unchecked")
	public Mouse(int maxMouseClickedEventsToRecord,
			 	 int maxMousePressedEventsToRecord,
			 	 int maxMouseReleasedEventsToRecord,
			 	 int maxMouseEnteredEventsToRecord,
			 	 int maxMouseExitedEventsToRecord,
			 	 int maxMouseDraggedEventsToRecord, 
			 	 int maxMouseMovedEventsToRecord, 
			 	 int maxMouseWheelMovedEventsToRecord, 
			 	 Container mouseSource)
	{
		// Set up max event count values for each event type
		maxEvents = new int[MouseEventType.values().length];
		maxEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()] = maxMouseClickedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()] = maxMousePressedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()] = maxMouseReleasedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()] = maxMouseEnteredEventsToRecord;
		maxEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()] = maxMouseExitedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()] = maxMouseExitedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()] = maxMouseExitedEventsToRecord;
		maxEvents[MouseEventType.MOUSE_WHEEL_EVENT.ordinal()] = maxMouseWheelMovedEventsToRecord;

		// MMMM SUPER HACK
		// THANKS FOR NOT ALLOWING ARRAYS OF GENERICS JAVA
		// I AM GLAD TO BE USING THIS SOLUTION
		//
		allMouseEvents = (LinkedList<MouseEvent>[]) new LinkedList[MouseEventType.values().length];

		allMouseEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()] > 0)
											   ? new LinkedList<MouseEvent>()
											   : null);
		allMouseEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()] > 0)
				   							   ? new LinkedList<MouseEvent>()
				   							   : null);
		allMouseEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()] > 0)
				   							   ? new LinkedList<MouseEvent>()
				   							   : null);
		allMouseEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()] > 0)
				   							  ? new LinkedList<MouseEvent>()
				   							  : null);
		allMouseEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()] > 0)
				   						     ? new LinkedList<MouseEvent>()
				   						     : null);
		allMouseEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()] > 0)
					  						  ? new LinkedList<MouseEvent>()
					  						  : null);
		allMouseEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()] = ((maxEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()] > 0)
				     						 ? new LinkedList<MouseEvent>()
				     						 : null);
		allMouseWheelEvents = ((maxEvents[MouseEventType.MOUSE_WHEEL_EVENT.ordinal()] > 0)
				     		  ? new LinkedList<MouseWheelEvent>()
   						      : null);
		
		// Register the Component that will be using this Keyboard, if able
		if((parent = mouseSource) != null)
		{
			parent.addMouseListener(this);
			parent.addMouseMotionListener(this);
			parent.addMouseWheelListener(this);
		}
	}
	
	/*******************************************************************
	 * 
	 * INHERITED METHODS MouseEventType.
	 * 
	 *******************************************************************/

	/**
	 * 
	 */
	public void mouseClicked(MouseEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_CLICKED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_CLICKED_EVENT.ordinal()].removeFirst();
	}

	/**
	 * 
	 */
	public void mousePressed(MouseEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_PRESSED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_PRESSED_EVENT.ordinal()].removeFirst();
	}

	/**
	 * 
	 */
	public void mouseReleased(MouseEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_RELEASED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_RELEASED_EVENT.ordinal()].removeFirst();
	}

	/**
	 * 
	 */
	public void mouseEntered(MouseEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_ENTERED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_ENTERED_EVENT.ordinal()].removeFirst();
	}

	/**
	 * 
	 */
	public void mouseExited(MouseEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_EXITED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_EXITED_EVENT.ordinal()].removeFirst();
	}
	
	/**
	 * 
	 */
	public void mouseDragged(MouseEvent e)
	{
		if(maxEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_DRAGGED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_DRAGGED_EVENT.ordinal()].removeFirst();
	}
	
	/**
	 * 
	 */
	public void mouseMoved(MouseEvent e)
	{
		if(maxEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()] > 0) allMouseEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()].add(e);
		
		onMouseEvent(MouseEventType.MOUSE_MOVED_EVENT, e);
		
		while(maxEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()] > 0 && allMouseEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()].size() > maxEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()])
			allMouseEvents[MouseEventType.MOUSE_MOVED_EVENT.ordinal()].removeFirst();
	}
	
	/**
	 * 
	 */
	public void mouseWheelMoved(MouseWheelEvent e) 
	{
		if(maxEvents[MouseEventType.MOUSE_WHEEL_EVENT.ordinal()] > 0) allMouseWheelEvents.add(e);
		
		onMouseWheelEvent(e);
		
		while(maxEvents[MouseEventType.MOUSE_WHEEL_EVENT.ordinal()] > 0 && allMouseWheelEvents.size() > maxEvents[MouseEventType.MOUSE_WHEEL_EVENT.ordinal()])
			allMouseWheelEvents.removeFirst();
	}
	
	/*******************************************************************
	 * 
	 * ABSTRACT METHODS
	 * 
	 *******************************************************************/

	/**
	 * 
	 */
	public abstract void onMouseEvent(MouseEventType t, MouseEvent e);
	
	/**
	 * 
	 * @param e
	 */
	public abstract void onMouseWheelEvent(MouseWheelEvent e);
	
	/*******************************************************************
	 * 
	 * UTILITIES
	 * 
	 *******************************************************************/
	
	/**
	 * 
	 * @param mouseEventType
	 * @return
	 */
	public final int numberOfMouseEvents(MouseEventType t)
	{
		// Doing some quick checks to see if any processing is really required.
		//
		if (t == null ||
			(MouseEventType.MOUSE_WHEEL_EVENT.equals(t)
			 ? allMouseWheelEvents == null 
			 : allMouseEvents[t.ordinal()] == null)) return 0;
		
		return (MouseEventType.MOUSE_WHEEL_EVENT.equals(t)
				? allMouseWheelEvents.size()
				: allMouseEvents[t.ordinal()].size());
	}
	
	/**
	 * 
	 * @param mouseEventType
	 * @return
	 */
	public final boolean isRecordingEventType(MouseEventType t)
	{
		return (t != null && maxEvents[t.ordinal()] > 0);
	}
	
	/*******************************************************************
	 * 
	 * SETTERS / GETTERS / ACCESSORS
	 * 
	 *******************************************************************/
	
	/** <code>getParent</code><br>
	 * <br>
	 * Returns the <code>Container</code> that is currently using this <code>Keyboard</code>. This 
	 * method will return null if no <code>Container</code> is registered.<br>
	 * <br>
	 * 
	 */
	public final Container getParent() { return parent; }
	
	/** <code>registerParent</code><br>
	 * <br>
	 * Registers the given <code>Container</code> as the parent using this <code>Mouse</code>, 
	 * and adds this <code>Mouse</code> as a mouse listener to the <code>Container</code>.<br>
	 * Each <code>Mouse</code> object may only have one parent at a time; if this 
	 * <code>Mouse</code> has a non-null parent, no changes are made.<br>
	 * <br>
	 * 
	 * @param keyListenerUser
	 */
	public final void registerParent(Container mouseUser)
	{
		// Ha ha that was funny
		// Didn't put brackets around the first 'if', and java thought the 'else' was for
		// the second 'if'.
		// How embarrassing!
		//
		if(parent == null && (parent = mouseUser) != null)
		{
			parent.addMouseListener(this);
			parent.addMouseMotionListener(this);
			parent.addMouseWheelListener(this);
		}
		else
			System.out.println("Could not register given Container to this Mouse.");
	}
	
	/** <code>unregisterParent</code><br>
	 * <br>
	 * Unregisters and returns the <code>Container</code> using this <code>Mouse</code>, and also
	 * removes this <code>Mouse</code> from the <code>Container's</code> set of key 
	 * listeners.<br>
	 * <br>
	 * Returns null if no parent <code>Container</code> is registered.<br>
	 * <br>
	 * 
	 * @return
	 */
	public final Container unregisterParent()
	{
		Container old = parent;
		
		if(parent != null)
		{
			parent.removeMouseListener(this);
			parent.removeMouseMotionListener(this);
			parent.removeMouseWheelListener(this);
			parent = null;
		}
		
		return old;
	}
	
	/**
	 * 
	 */
	public final MouseEvent getLastMouseEvent(MouseEventType t)
	{		
		if(t != null)
		{
			if(MouseEventType.MOUSE_WHEEL_EVENT.equals(t))
			{
				if(allMouseWheelEvents != null && allMouseWheelEvents.size() > 0)
					return allMouseWheelEvents.getLast();
			}
			else
			{
				if(allMouseEvents[t.ordinal()] != null && allMouseEvents[t.ordinal()].size() > 0)
					return allMouseEvents[t.ordinal()].getLast();
			}
		}
		
		return null;
	}
	
	/**
	 * 
	 * @param mouseEventType
	 * @param nthLastEvent
	 * @return
	 */
	public final MouseEvent getNthLastKeyEvent(MouseEventType t, int nthLastEvent)
	{
		if(t != null)
		{
			if(MouseEventType.MOUSE_WHEEL_EVENT.equals(t))
			{
				if(allMouseWheelEvents != null && allMouseWheelEvents.size() >= nthLastEvent)
					return allMouseWheelEvents.get(allMouseWheelEvents.size() - nthLastEvent);
			}
			else
			{
				if(allMouseEvents[t.ordinal()] != null && allMouseEvents[t.ordinal()].size() >= nthLastEvent)
					return allMouseEvents[t.ordinal()].get(allMouseEvents[t.ordinal()].size() - nthLastEvent);
			}
		}
		
		return null;
	}
	
	/**
	 * 
	 * @param mouseEventType
	 * @param timeStart
	 * @param timeEnd
	 * @return
	 */
	public final ArrayList<MouseEvent> getEventsInTimeSpan(MouseEventType t, long timeStart, long timeEnd)
	{
		// Doing some quick checks to see if any processing is really required.
		//
		if (t == null) return null;
		if (MouseEventType.MOUSE_WHEEL_EVENT.equals(t) && (allMouseWheelEvents == null || allMouseWheelEvents.size() < 1)) return null;
		if (!MouseEventType.MOUSE_WHEEL_EVENT.equals(t) && (allMouseEvents[t.ordinal()] == null || allMouseEvents[t.ordinal()].size() < 1)) return null;
		if (timeEnd < timeStart || timeEnd < 1) return null;
		
		// Set up variables
		//
		ArrayList<MouseEvent> results = new ArrayList<MouseEvent>();
		MouseEvent iterEvent = null;
		
		// Iterator should go from the end of the list (most recent) to the start of the list (least recent).
		//
		@SuppressWarnings("unchecked")
		Iterator<MouseEvent> eventsList = (Iterator<MouseEvent>) ((MouseEventType.MOUSE_WHEEL_EVENT.equals(t)) ? allMouseWheelEvents.descendingIterator() : allMouseEvents[t.ordinal()].descendingIterator());

		// Loop over all mouse events and look for any mouse events that fall within the specified 
		// time range.
		//
		while(eventsList != null && eventsList.hasNext())
		{
			// Make the iterator go a little faster.
			// - Keep going if nulls are found.
			// - If a non-null is found, keep going if it is still outside of the specified time range.
			//
			do
			{
				while((iterEvent = eventsList.next()) == null && eventsList.hasNext());
			}
			while(iterEvent.getWhen() > timeEnd && eventsList.hasNext());
			
			// Verify that the mouse event's occurrence falls within the specified time range.
			//
			if(iterEvent != null && iterEvent.getWhen() >= timeStart && iterEvent.getWhen() <= timeEnd)
				results.add(iterEvent);
			
			// Loop breaking condition:
			// - If any MouseEvent is reached such that its recorded time is older time than the lower time
			// range limit, this loop will break.
			// - It is assumed that all pairs recorded are recorded in sequential order of occurrence.
			//
			if(iterEvent != null && iterEvent.getWhen() < timeStart)
				eventsList = null;
		}
		
		// Return non-null only if there are any results to give.
		//
		return (results.size() > 0 ? results : null);
	}
}
